// Copyright 2013 The Flutter Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:math' as math;

import 'package:collection/collection.dart';
import 'package:file/file.dart';
import 'package:meta/meta.dart';
import 'package:pub_semver/pub_semver.dart';

import 'common/core.dart';
import 'common/output_utils.dart';
import 'common/package_looping_command.dart';
import 'common/plugin_utils.dart';
import 'common/repository_package.dart';

/// The lowest `ext.kotlin_version` that example apps are allowed to use.
@visibleForTesting
final Version minKotlinVersion = Version(1, 7, 10);

/// A command to enforce gradle file conventions and best practices.
class GradleCheckCommand extends PackageLoopingCommand {
  /// Creates an instance of the gradle check command.
  GradleCheckCommand(super.packagesDir, {super.gitDir});

  static const int _minimumJavaVersion = 17;

  @override
  final String name = 'gradle-check';

  @override
  List<String> get aliases => <String>['check-gradle'];

  @override
  final String description =
      'Checks that gradle files follow repository conventions.';

  @override
  bool get hasLongOutput => false;

  @override
  PackageLoopingType get packageLoopingType =>
      PackageLoopingType.includeAllSubpackages;

  @override
  Future<PackageResult> runForPackage(RepositoryPackage package) async {
    if (!package.platformDirectory(FlutterPlatform.android).existsSync()) {
      return PackageResult.skip('No android/ directory.');
    }

    const exampleDirName = 'example';
    final bool isExample =
        package.directory.basename == exampleDirName ||
        package.directory.parent.basename == exampleDirName;
    if (!_validateBuildGradles(package, isExample: isExample)) {
      return PackageResult.fail();
    }
    return PackageResult.success();
  }

  bool _validateBuildGradles(
    RepositoryPackage package, {
    required bool isExample,
  }) {
    final Directory androidDir = package.platformDirectory(
      FlutterPlatform.android,
    );
    final File topLevelGradleFile = _getBuildGradleFile(androidDir);

    // This is tracked as a variable rather than a sequence of &&s so that all
    // failures are reported at once, not just the first one.
    var succeeded = true;
    if (isExample) {
      if (!_validateExampleTopLevelBuildGradle(package, topLevelGradleFile)) {
        succeeded = false;
      }
      final File topLevelSettingsGradleFile = _getSettingsGradleFile(
        androidDir,
      );
      if (!_validateExampleTopLevelSettingsGradle(
        package,
        topLevelSettingsGradleFile,
      )) {
        succeeded = false;
      }

      final File appGradleFile = _getBuildGradleFile(
        androidDir.childDirectory('app'),
      );
      if (!_validateExampleAppBuildGradle(package, appGradleFile)) {
        succeeded = false;
      }
    } else {
      succeeded = _validatePluginBuildGradle(package, topLevelGradleFile);
    }

    return succeeded;
  }

  // Returns the gradle file in the given directory.
  File _getBuildGradleFile(Directory dir) => dir.childFile('build.gradle');

  // Returns the settings gradle file in the given directory.
  File _getSettingsGradleFile(Directory dir) =>
      dir.childFile('settings.gradle');

  // Returns the main/AndroidManifest.xml file for the given package.
  File _getMainAndroidManifest(
    RepositoryPackage package, {
    required bool isExample,
  }) {
    final Directory androidDir = package.platformDirectory(
      FlutterPlatform.android,
    );
    final Directory baseDir = isExample
        ? androidDir.childDirectory('app')
        : androidDir;
    return baseDir
        .childDirectory('src')
        .childDirectory('main')
        .childFile('AndroidManifest.xml');
  }

  bool _isCommented(String line) => line.trim().startsWith('//');

  /// Validates the build.gradle file for a plugin
  /// (some_plugin/android/build.gradle).
  bool _validatePluginBuildGradle(RepositoryPackage package, File gradleFile) {
    print(
      '${indentation}Validating '
      '${getRelativePosixPath(gradleFile, from: package.directory)}.',
    );
    final String contents = gradleFile.readAsStringSync();
    final List<String> lines = contents.split('\n');

    // This is tracked as a variable rather than a sequence of &&s so that all
    // failures are reported at once, not just the first one.
    var succeeded = true;
    if (!_validateNamespace(package, contents, isExample: false)) {
      succeeded = false;
    }
    if (!_validateCompatibilityVersions(lines)) {
      succeeded = false;
    }
    if (!_validateKotlinJvmCompatibility(lines)) {
      succeeded = false;
    }
    if (!_validateJavaKotlinCompileOptionsAlignment(lines)) {
      succeeded = false;
    }
    if (!_validateGradleDrivenLintConfig(package, lines)) {
      succeeded = false;
    }
    if (!_validateCompileSdkUsage(package, lines)) {
      succeeded = false;
    }
    return succeeded;
  }

  /// Documentation url for Artifact hub implementation in flutter repo's.
  @visibleForTesting
  static const String artifactHubDocumentationString =
      r'https://github.com/flutter/flutter/blob/master/docs/ecosystem/Plugins-and-Packages-repository-structure.md#gradle-structure';

  /// String printed as example of valid example root build.gradle repository
  /// configuration that enables artifact hub env variable.
  @visibleForTesting
  static const String exampleRootGradleArtifactHubString =
      '''
        // See $artifactHubDocumentationString for more info.
        def artifactRepoKey = 'ARTIFACT_HUB_REPOSITORY'
        if (System.getenv().containsKey(artifactRepoKey)) {
            println "Using artifact hub"
            maven { url System.getenv(artifactRepoKey) }
        }
''';

  /// Validates that [gradleLines] reads and uses a artifiact hub repository
  /// when ARTIFACT_HUB_REPOSITORY is set.
  ///
  /// Required in root gradle file.
  bool _validateArtifactHubUsage(
    RepositoryPackage example,
    List<String> gradleLines,
  ) {
    // Gradle variable name used to hold environment variable string.
    const keyVariable = 'artifactRepoKey';
    final keyPresentRegex = RegExp(
      '$keyVariable'
      r"\s+=\s+'ARTIFACT_HUB_REPOSITORY'",
    );
    final documentationPresentRegex = RegExp(
      r'github\.com.*flutter.*blob.*Plugins-and-Packages-repository-structure.*gradle-structure',
    );
    final keyReadRegex = RegExp(
      r'if.*System\.getenv.*\.containsKey.*'
      '$keyVariable',
    );
    final keyUsedRegex = RegExp(
      r'maven.*url.*System\.getenv\('
      '$keyVariable',
    );

    final bool keyPresent = gradleLines.any(
      (String line) => keyPresentRegex.hasMatch(line),
    );
    final bool documentationPresent = gradleLines.any(
      (String line) => documentationPresentRegex.hasMatch(line),
    );
    final bool keyRead = gradleLines.any(
      (String line) => keyReadRegex.hasMatch(line),
    );
    final bool keyUsed = gradleLines.any(
      (String line) => keyUsedRegex.hasMatch(line),
    );

    if (!(documentationPresent && keyPresent && keyRead && keyUsed)) {
      printError(
        'Failed Artifact Hub validation. Include the following in '
        'example root build.gradle:\n$exampleRootGradleArtifactHubString',
      );
    }

    return keyPresent && documentationPresent && keyRead && keyUsed;
  }

  /// Validates the top-level settings.gradle for an example app (e.g.,
  /// some_package/example/android/settings.gradle).
  bool _validateExampleTopLevelSettingsGradle(
    RepositoryPackage package,
    File gradleSettingsFile,
  ) {
    print(
      '${indentation}Validating '
      '${getRelativePosixPath(gradleSettingsFile, from: package.directory)}.',
    );
    final String contents = gradleSettingsFile.readAsStringSync();
    final List<String> lines = contents.split('\n');
    // This is tracked as a variable rather than a sequence of &&s so that all
    // failures are reported at once, not just the first one.
    var succeeded = true;
    if (!_validateArtifactHubSettingsUsage(package, lines)) {
      succeeded = false;
    }
    return succeeded;
  }

  /// String printed as a valid example of settings.gradle repository
  /// configuration that enables artifact hub env variable.
  @visibleForTesting
  static String exampleSettingsArtifactHubString = '''
plugins {
    id "dev.flutter.flutter-plugin-loader" version "1.0.0"
    // ...other plugins
    id "com.google.cloud.artifactregistry.gradle-plugin" version "2.2.1"
}
  ''';

  /// Validates that [gradleLines] reads and uses a artifiact hub repository
  /// when ARTIFACT_HUB_REPOSITORY is set.
  ///
  /// Required in root gradle file.
  bool _validateArtifactHubSettingsUsage(
    RepositoryPackage example,
    List<String> gradleLines,
  ) {
    final documentationPresentRegex = RegExp(
      r'github\.com.*flutter.*blob.*Plugins-and-Packages-repository-structure.*gradle-structure',
    );
    final artifactRegistryPluginApplyRegex = RegExp(
      r'id.*com\.google\.cloud\.artifactregistry\.gradle-plugin.*version.*\b\d+\.\d+\.\d+\b',
    );

    final bool documentationPresent = gradleLines.any(
      (String line) => documentationPresentRegex.hasMatch(line),
    );
    final bool declarativeArtifactRegistryApplied = gradleLines.any(
      (String line) => artifactRegistryPluginApplyRegex.hasMatch(line),
    );
    final bool validArtifactConfiguration =
        documentationPresent && declarativeArtifactRegistryApplied;

    if (!validArtifactConfiguration) {
      printError('Failed Artifact Hub validation.');
      if (!documentationPresent) {
        printError(
          'The link to the Artifact Hub documentation is missing. Include the following in '
          'example root settings.gradle:\n// See $artifactHubDocumentationString for more info.',
        );
      }
      if (!declarativeArtifactRegistryApplied) {
        printError(
          'Include the following in '
          'example root settings.gradle:\n$exampleSettingsArtifactHubString',
        );
      }
    }
    return validArtifactConfiguration;
  }

  /// Validates the top-level build.gradle for an example app (e.g.,
  /// some_package/example/android/build.gradle).
  bool _validateExampleTopLevelBuildGradle(
    RepositoryPackage package,
    File gradleFile,
  ) {
    print(
      '${indentation}Validating '
      '${getRelativePosixPath(gradleFile, from: package.directory)}.',
    );
    final String contents = gradleFile.readAsStringSync();
    final List<String> lines = contents.split('\n');

    // This is tracked as a variable rather than a sequence of &&s so that all
    // failures are reported at once, not just the first one.
    var succeeded = true;
    if (!_validateJavacLintConfig(package, lines)) {
      succeeded = false;
    }
    if (!_validateKotlinVersion(package, lines)) {
      succeeded = false;
    }
    if (!_validateArtifactHubUsage(package, lines)) {
      succeeded = false;
    }
    return succeeded;
  }

  /// Validates the app-level build.gradle for an example app (e.g.,
  /// some_package/example/android/app/build.gradle).
  bool _validateExampleAppBuildGradle(
    RepositoryPackage package,
    File gradleFile,
  ) {
    print(
      '${indentation}Validating '
      '${getRelativePosixPath(gradleFile, from: package.directory)}.',
    );
    final String contents = gradleFile.readAsStringSync();

    // This is tracked as a variable rather than a sequence of &&s so that all
    // failures are reported at once, not just the first one.
    var succeeded = true;
    if (!_validateNamespace(package, contents, isExample: true)) {
      succeeded = false;
    }
    return succeeded;
  }

  /// Validates that [gradleContents] sets a namespace, which is required for
  /// compatibility with apps that use AGP 8+.
  bool _validateNamespace(
    RepositoryPackage package,
    String gradleContents, {
    required bool isExample,
  }) {
    // Regex to validate that the following namespace definition
    // is found (where the single quotes can be single or double):
    //  - namespace = 'dev.flutter.foo'
    final nameSpaceRegex = RegExp(
      '^\\s*namespace\\s+=\\s*[\'"](.*?)[\'"]',
      multiLine: true,
    );
    final RegExpMatch? nameSpaceRegexMatch = nameSpaceRegex.firstMatch(
      gradleContents,
    );

    if (nameSpaceRegexMatch == null) {
      const errorMessage = '''
build.gradle must set a "namespace":

    android {
        namespace = "dev.flutter.foo"
    }

The value must match the "package" attribute in AndroidManifest.xml, if one is
present. For more information, see:
https://developer.android.com/build/publish-library/prep-lib-release#choose-namespace
''';

      printError(
        '$indentation${errorMessage.split('\n').join('\n$indentation')}',
      );
      return false;
    } else {
      return _validateNamespaceMatchesManifest(
        package,
        isExample: isExample,
        namespace: nameSpaceRegexMatch.group(1)!,
      );
    }
  }

  /// Validates that the given namespace matches the manifest package of
  /// [package] (if any; a package does not need to be in the manifest in cases
  /// where compatibility with AGP <7 is no longer required).
  ///
  /// Prints an error and returns false if validation fails.
  bool _validateNamespaceMatchesManifest(
    RepositoryPackage package, {
    required bool isExample,
    required String namespace,
  }) {
    final manifestPackageRegex = RegExp(r'package\s*=\s*"(.*?)"');
    final String manifestContents = _getMainAndroidManifest(
      package,
      isExample: isExample,
    ).readAsStringSync();
    final RegExpMatch? packageMatch = manifestPackageRegex.firstMatch(
      manifestContents,
    );
    if (packageMatch != null && namespace != packageMatch.group(1)) {
      final errorMessage =
          '''
build.gradle "namespace" must match the "package" attribute in AndroidManifest.xml, if one is present.
  build.gradle namespace: "$namespace"
  AndroidMastifest.xml package: "${packageMatch.group(1)}"
''';
      printError(
        '$indentation${errorMessage.split('\n').join('\n$indentation')}',
      );
      return false;
    }
    return true;
  }

  /// Checks for a source compatibiltiy version, so that it's explicit rather
  /// than using whatever the client's local toolchaing defaults to (which can
  /// lead to compile errors that show up for clients, but not in CI).
  bool _validateCompatibilityVersions(List<String> gradleLines) {
    final bool hasLanguageVersion = gradleLines.any(
      (String line) => line.contains('languageVersion') && !_isCommented(line),
    );
    final bool hasCompabilityVersions =
        gradleLines.any(
          (String line) =>
              line.contains('sourceCompatibility = JavaVersion.VERSION_') &&
              !_isCommented(line),
        ) &&
        // Newer toolchains default targetCompatibility to the same value as
        // sourceCompatibility, but older toolchains require it to be set
        // explicitly. The exact version cutoff (and of which piece of the
        // toolchain; likely AGP) is unknown; for context see
        // https://github.com/flutter/flutter/issues/125482
        gradleLines.any(
          (String line) =>
              line.contains('targetCompatibility = JavaVersion.VERSION_') &&
              !_isCommented(line),
        );
    if (!hasLanguageVersion && !hasCompabilityVersions) {
      const javaErrorMessage =
          '''
build.gradle(.kts) must set an explicit Java compatibility version.

This can be done either via "sourceCompatibility"/"targetCompatibility":
    android {
        compileOptions {
            sourceCompatibility = JavaVersion.VERSION_$_minimumJavaVersion
            targetCompatibility = JavaVersion.VERSION_$_minimumJavaVersion
        }
    }

or "toolchain":
    java {
        toolchain {
            languageVersion = JavaLanguageVersion.of($_minimumJavaVersion)
        }
    }

See:
https://docs.gradle.org/current/userguide/java_plugin.html#toolchain_and_compatibility
for more details.''';

      printError(
        '$indentation${javaErrorMessage.split('\n').join('\n$indentation')}',
      );
      return false;
    }

    return true;
  }

  bool _validateKotlinJvmCompatibility(List<String> gradleLines) {
    bool isKotlinOptions(String line) =>
        line.contains('kotlinOptions') && !_isCommented(line);
    final bool hasKotlinOptions = gradleLines.any(isKotlinOptions);
    final bool kotlinOptionsUsesJavaVersion = gradleLines.any(
      (String line) =>
          line.contains('jvmTarget = JavaVersion.VERSION_') &&
          !_isCommented(line),
    );
    // Either does not set kotlinOptions or does and uses non-string based syntax.
    if (hasKotlinOptions && !kotlinOptionsUsesJavaVersion) {
      // Bad lines contains the first 4 lines including the kotlinOptions section.
      var badLines = '';
      final int startIndex = gradleLines.indexOf(
        gradleLines.firstWhere(isKotlinOptions),
      );
      for (
        var i = startIndex;
        i < math.min(startIndex + 4, gradleLines.length);
        i++
      ) {
        badLines += '${gradleLines[i]}\n';
      }
      final kotlinErrorMessage =
          '''
If build.gradle(.kts) sets jvmTarget then it must use JavaVersion syntax.
  Good:
    android {
      kotlinOptions {
          jvmTarget = JavaVersion.VERSION_$_minimumJavaVersion.toString()
      }
    }
  BAD:
    $badLines
''';
      printError(
        '$indentation${kotlinErrorMessage.split('\n').join('\n$indentation')}',
      );
      return false;
    }
    // No error condition.
    return true;
  }

  bool _validateJavaKotlinCompileOptionsAlignment(List<String> gradleLines) {
    final javaVersions = <String>[];
    // Some java versions have the format VERSION_1_8 but we dont need to handle those
    // because they are below the minimum.
    final javaVersionMatcher = RegExp(
      r'JavaVersion.VERSION_(?<javaVersion>\d+)',
    );
    for (final line in gradleLines) {
      final RegExpMatch? match = javaVersionMatcher.firstMatch(line);
      if (!_isCommented(line) && match != null) {
        final String? foundVersion = match.namedGroup('javaVersion');
        if (foundVersion != null) {
          javaVersions.add(foundVersion);
        }
      }
    }
    if (javaVersions.isNotEmpty) {
      final int version = int.parse(javaVersions.first);
      if (!javaVersions.every((String element) => element == '$version')) {
        const javaVersionAlignmentError = '''
If build.gradle(.kts) uses JavaVersion.* versions must be the same.
''';
        printError(
          '$indentation${javaVersionAlignmentError.split('\n').join('\n$indentation')}',
        );
        return false;
      }

      if (version < _minimumJavaVersion) {
        final minimumJavaVersionError =
            '''
build.gradle(.kts) uses "JavaVersion.VERSION_$version".
Which is below the minimum required. Use at least "JavaVersion.VERSION_$_minimumJavaVersion".
''';
        printError(
          '$indentation${minimumJavaVersionError.split('\n').join('\n$indentation')}',
        );
        return false;
      }
    }

    return true;
  }

  /// Returns whether the given gradle content is configured to enable all
  /// Gradle-driven lints (those checked by ./gradlew lint) and treat them as
  /// errors.
  bool _validateGradleDrivenLintConfig(
    RepositoryPackage package,
    List<String> gradleLines,
  ) {
    final List<String> gradleBuildContents = package
        .platformDirectory(FlutterPlatform.android)
        .childFile('build.gradle')
        .readAsLinesSync();
    if (!gradleBuildContents.any(
          (String line) =>
              line.contains('checkAllWarnings = true') && !_isCommented(line),
        ) ||
        !gradleBuildContents.any(
          (String line) =>
              line.contains('warningsAsErrors = true') && !_isCommented(line),
        )) {
      printError(
        '${indentation}This package is not configured to enable all '
        'Gradle-driven lint warnings and treat them as errors. '
        'Please add the following to the lintOptions section of '
        'android/build.gradle:',
      );
      print('''
        checkAllWarnings = true
        warningsAsErrors = true
''');
      return false;
    }
    return true;
  }

  bool _validateCompileSdkUsage(
    RepositoryPackage package,
    List<String> gradleLines,
  ) {
    final linePattern = RegExp(r'^\s*compileSdk.*\s+=');
    final legacySettingPattern = RegExp(r'^\s*compileSdkVersion');
    final String? compileSdkLine = gradleLines.firstWhereOrNull(
      (String line) => linePattern.hasMatch(line),
    );

    if (compileSdkLine == null) {
      // Equals regex not found check for method pattern.
      final compileSpacePattern = RegExp(r'^\s*compileSdk');
      final String? methodAssignmentLine = gradleLines.firstWhereOrNull(
        (String line) => compileSpacePattern.hasMatch(line),
      );
      if (methodAssignmentLine == null) {
        printError('${indentation}No compileSdk or compileSdkVersion found.');
      } else {
        printError(
          '${indentation}No "compileSdk =" found. Please use property assignment.',
        );
      }
      return false;
    }
    if (legacySettingPattern.hasMatch(compileSdkLine)) {
      printError(
        '${indentation}Please replace the deprecated '
        '"compileSdkVersion" setting with the newer "compileSdk"',
      );
      return false;
    }
    if (compileSdkLine.contains('flutter.compileSdkVersion')) {
      final Pubspec pubspec = package.parsePubspec();
      final VersionConstraint? flutterConstraint =
          pubspec.environment['flutter'];
      final Version? minFlutterVersion =
          flutterConstraint != null && flutterConstraint is VersionRange
          ? flutterConstraint.min
          : null;
      if (minFlutterVersion == null) {
        printError(
          '${indentation}Unable to find a Flutter SDK version '
          'constraint. Use of flutter.compileSdkVersion requires a minimum '
          'Flutter version of 3.27',
        );
        return false;
      }
      if (minFlutterVersion < Version(3, 27, 0)) {
        printError(
          '${indentation}Use of flutter.compileSdkVersion requires a '
          'minimum Flutter version of 3.27, but this package currently '
          'supports $minFlutterVersion.\n'
          "${indentation}Please update the package's minimum Flutter SDK "
          'version to at least 3.27.',
        );
        return false;
      }
    } else {
      // Extract compileSdkVersion and check if it is higher than flutter.compileSdkVersion.
      final numericVersionPattern = RegExp(r'=\s*(\d+)');
      final RegExpMatch? versionMatch = numericVersionPattern.firstMatch(
        compileSdkLine,
      );

      if (versionMatch != null) {
        final int compileSdkVersion = int.parse(versionMatch.group(1)!);
        const minCompileSdkVersion = 36;

        if (compileSdkVersion < minCompileSdkVersion) {
          printError(
            '${indentation}compileSdk version $compileSdkVersion is too low. '
            'Minimum required version is $minCompileSdkVersion.\n'
            "${indentation}Please update this package's compileSdkVersion to at least "
            '$minCompileSdkVersion or use flutter.compileSdkVersion.',
          );
          return false;
        }
      } else {
        printError('${indentation}Unable to parse compileSdk version number.');
        return false;
      }
    }
    return true;
  }

  /// Validates whether the given [example]'s gradle content is configured to
  /// build its plugin target with javac lints enabled and treated as errors,
  /// if the enclosing package is a plugin.
  ///
  /// This can only be called on example packages. (Plugin packages should not
  /// be configured this way, since it would affect clients.)
  ///
  /// If [example]'s enclosing package is not a plugin package, this just
  /// returns true.
  bool _validateJavacLintConfig(
    RepositoryPackage example,
    List<String> gradleLines,
  ) {
    final RepositoryPackage enclosingPackage = example.getEnclosingPackage()!;
    if (!pluginSupportsPlatform(
      platformAndroid,
      enclosingPackage,
      requiredMode: PlatformSupport.inline,
    )) {
      return true;
    }
    final String enclosingPackageName = enclosingPackage.directory.basename;

    // The check here is intentionally somewhat loose, to allow for the
    // possibility of variations (e.g., not using Xlint:all in some cases, or
    // passing other arguments).
    if (!(gradleLines.any(
          (String line) => line.contains('project(":$enclosingPackageName")'),
        ) &&
        gradleLines.any(
          (String line) =>
              line.contains('options.compilerArgs') &&
              line.contains('-Xlint') &&
              line.contains('-Werror'),
        ))) {
      printError(
        'The example '
        '"${getRelativePosixPath(example.directory, from: enclosingPackage.directory)}" '
        'is not configured to treat javac lints and warnings as errors. '
        'Please add the following to its build.gradle:',
      );
      print('''
gradle.projectsEvaluated {
    project(":$enclosingPackageName") {
        tasks.withType(JavaCompile) {
            options.compilerArgs << "-Xlint:all" << "-Werror"
        }
    }
}
''');
      return false;
    }
    return true;
  }

  /// Validates whether the given [example] has its Kotlin version set to at
  /// least a minimum value, if it is set at all.
  bool _validateKotlinVersion(
    RepositoryPackage example,
    List<String> gradleLines,
  ) {
    final kotlinVersionRegex = RegExp(r"ext\.kotlin_version\s*=\s*'([\d.]+)'");
    RegExpMatch? match;
    if (gradleLines.any((String line) {
      match = kotlinVersionRegex.firstMatch(line);
      return match != null;
    })) {
      final version = Version.parse(match!.group(1)!);
      if (version < minKotlinVersion) {
        printError(
          'build.gradle sets "ext.kotlin_version" to "$version". The '
          'minimum Kotlin version that can be specified is '
          '$minKotlinVersion, for compatibility with modern dependencies.',
        );
        return false;
      }
    }
    return true;
  }
}
